function isArray(val) {
  return {}.toString.call(val) === '[object Array]';
}

/**
 * 管理事件的注册和触发
 *
 * @class EventEmitter 管理事件的注册和触发
 * @example
 *  var ee = new EventEmitter()
 *  function log (arg) {
 *    console.log(arg)
 *  }
 *  ee.on('log', log, window)
 *  ee.emit('log', 'i am a log') // i am a log
 *  ee.off('log')
 *  ee.emit('log', 'i am a log too') // undefined
 */
export default class EventEmitter {
  constructor() {
    /** @type {object} */
    this.events = {};
  }

  /**
   * 给指定的事件添加监听器函数
   * 不会重复添加已经存在的事件
   *
   * @param {string} type 要添加到监听器中的事件名称
   * @param {function} fn 监听的时间函数
   * @param {object} context 事件所处的上下文环境
   * @return {object} 当前EventEmitter实例对象链
   */
  on(type, fn, context) {
    if (typeof fn !== 'function') throw new TypeError('The listener must be a function');
    const item = [context, fn];
    if (isArray(this.events[type])) {
      this.events[type].push(item);
    } else {
      this.events[type] = [item];
    }
    return this;
  }

  /**
   * 从监听器中删除指定的监听事件
   *
   * @param {string} type 要从监听器中删除的事件名称
   * @param {object} context 事件所处的上下文环境
   * @return {object} 当前EventEmitter实例对象链
   */
  off(type) {
    const evts = this.events[type];
    if (isArray(evts)) {
      delete this.events[type];
    }
    return this;
  }

  /**
   * 从监听器中删除所有的监听事件
   *
   * @return {object} 当前EventEmitter实例对象链
   */
  offAll() {
    this.events = {};
    return this;
  }

  /**
   * 触发事件
   *
   * @param {string} type 监听器要触发的的事件名称
   * @param  {...any} args 给要触发的事件函数传递的参数
   * @return {object} 当前EventEmitter实例对象链
   */
  emit(type, ...args) {
    const evts = this.events[type];
    if (isArray(evts)) {
      evts.forEach((item) => {
        item[1].apply(item[0], args);
      });
    }
    return this;
  }

  /**
   * 给指定的事件添加一次监听函数
   *
   * @param {string} type 要添加到监听器中的事件名称
   * @param {function} fn 监听的时间函数
   * @param {object} context 事件所处的上下文环境
   * @return {object} 当前EventEmitter实例对象链
   */
  once(type, fn, context) {
    this.on(
      type,
      function handler(...args) {
        this.off(type, context);
        fn.apply(context, args);
      }.bind(this),
      context,
    );
    return this;
  }
}
